package com.streetsmart;

/**
 * 
 * @author bhavani sudha
 *
 */
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Vector;

import org.json.JSONException;

import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.util.Log;

import com.streetsmart.main.SSScheduleDisplayActivity;

/**
 * 
 * Computes the schedule based on the time available, start and return locations
 * and also list of tasks
 * 
 */

public class Algorithm extends
		AsyncTask<Object, String, ArrayList<ScheduleListItem>> {
	private long startTime;
	// private float timeInHand;
	private long endTime;
	private MyLocation startAddress;
	private MyLocation returnAddress;
	private ArrayList<ScheduleListItem> scheduleList;
	private DBManager dbmanager;
	private ArrayList<MyEvent> mustToDos;
	private ArrayList<MyEvent> niceToDos;
	private ArrayList<MyEvent> furtherNiceToDos; // need to add code to also
													// consider further nice to
													// do things
	private ArrayList<MyEvent> inProgress; // refers to those items in schedule
											// which were started and aborted
											// due to rescheduling
	private Context context;
	private ProgressDialog progressDialog;
	private Calendar startCal;
	private Calendar endCal;
	MyEvent inprgs;
	private SSScheduleDisplayActivity callerActivity;

	public void attach(SSScheduleDisplayActivity activity) {
		this.callerActivity = activity;
	}

	public void detach() {
		callerActivity = null;
	}

	public Algorithm(float timeInHand, MyLocation startAddress,
			MyLocation returnAddress, Context c, SSScheduleDisplayActivity act) {
		attach(act);
		this.context = c;
		Utilities.setSchedule(null);
		dbmanager = new DBManager(context);

		startTime = Utilities.getCurrentTime() + (3 * 60 * 1000); // Allowing
		// 5
		// mins
		// buffer
		// time from
		// when he
		// will
		// start

		// startTime = Utilities.getCurrentTime();

		// this.timeInHand = timeInHand;
		endTime = Utilities.convertHourstoMillis(timeInHand) + startTime;
		this.returnAddress = returnAddress;
		this.startAddress = startAddress;
	}

	/*
	 * compute the schedule by trying to fit events/tasks between every pair of
	 * already knows tasks
	 */
	public ArrayList<ScheduleListItem> computeSchedule() throws JSONException {

		// Creating an empty schedule. Pushing in the start location to it.
		scheduleList = new ArrayList<ScheduleListItem>();
		MyEvent startEvent = new MyEvent("StartEvent", "other", startAddress,
				Utilities.convertMillistoDate(startTime),
				Utilities.convertMillistoDate(startTime), "Must");
		ScheduleListItem item = new ScheduleListItem(startEvent,
				Utilities.convertMillistoDate(startTime));
		scheduleList.add(item);

		inProgress = dbmanager.getAllInProgressEvents(startTime, endTime);

		if (inProgress.size() > 0) { // assumption: there can be only one event
										// which is under progress
			inprgs = inProgress.get(0);
			if (inprgs.getType().equals("Must")) {
				String[] result = Utilities.getDistance(this.startAddress,
						inprgs.getLocation());
				long timeToReachInPrgsEvent = Long.parseLong(result[1]);
				inprgs.setTimeToReachThis(timeToReachInPrgsEvent);
				inprgs.setStartTime(Utilities.convertMillistoDate(startEvent
						.getEndTime().getTime() + timeToReachInPrgsEvent));
				inprgs.setEndTime(Utilities.convertMillistoDate(inprgs
						.getStartTime().getTime() + inprgs.getDuration()));
				ScheduleListItem itemInProgress = new ScheduleListItem(inprgs,
						inprgs.getStartTime());
				scheduleList.add(itemInProgress);
				startTime = inprgs.getEndTime().getTime();
				this.startAddress = inprgs.getLocation();
			} else {
				dbmanager.updateEventStatus(inprgs.getId(), "unscheduled");
			}

		}

		// Leaving out those pending unschedules tasks from previous days. Need
		// to address them later.
		mustToDos = dbmanager.getAllMustToDoEventsListBetween(startTime,
				endTime);
		niceToDos = dbmanager.getAllGoodToDoEventsListBetween(startTime,
				endTime);
		furtherNiceToDos = dbmanager.getAllGoodToDoEventsListNotInBetween(
				startTime, endTime);

		// Creating an event for the return location and pushing it to the
		// mustToDos List.
		MyEvent endEvent = new MyEvent("EndEvent", "other", returnAddress,
				Utilities.convertMillistoDate(endTime),
				Utilities.convertMillistoDate(endTime), "Must");
		mustToDos.add(endEvent);

		// probably think of doing a preprocessing here for the first pair start
		// to Must1 before entering into the loop.
		MyEvent first;
		MyEvent second;
		if (scheduleList.size() == 2) {
			second = inprgs;
		} else {
			second = startEvent;
		}
		for (int i = 0; i < mustToDos.size(); i++) {
			first = second;
			second = mustToDos.get(i);
			fitEnroute(first, second);
		}

		runScheduleCompaction();

		return scheduleList;
	}

	public void fitEnroute(MyEvent First, MyEvent Second) throws JSONException {
		// Assumption Second is a Must type event.
		// Need to think logic for no Must type events
		// Also logic when no MustType events and starting and returning
		// locations are same

		/*
		 * find the tiem in hand = start time of second - end time of first find
		 * distance between first and second in terms of time to travel calulate
		 * buffer = tme in hand - distance between both based on buffer
		 * determine radius of search if buffer is less than 30 mins add second
		 * to the schedule list. change the state of it to scheduled and return
		 * back to caller else while either selected or reached end of list {
		 * grab the next unscheduled good to do event. If the event has a
		 * location determine if distance from first to location + duration of
		 * this new event (30 mins) + distance from this event to second is <=
		 * time in hand else use the radius near second to find businesses for
		 * each business { determine if distance f6rom first to location +
		 * duration of this new event (30 mins) + distance from this event to
		 * second is <= time in hand } } if suppose we reach end of list and
		 * still none is selected then try the further nice to do list
		 * 
		 * then call findEnroute(first, new) followed by fitEnroute(new, second)
		 */
		startCal = Calendar.getInstance();
		startCal.setTimeInMillis(First.getEndTime().getTime());
		// ec = Calendar.getInstance();
		// ec.setTimeInMillis(Second.getStartTime().getTime());

		long timeAvailable = Second.getStartTime().getTime()
				- First.getEndTime().getTime();
		long timeTo2From1;
		String temp[] = Utilities.getDistance(First.getLocation(),
				Second.getLocation());
		timeTo2From1 = Long.parseLong(temp[1]); // we can resue temp from now.
		long buffer = timeAvailable - timeTo2From1;
		// if buffer is negative (meaning we dont even have enough time to trael
		// to the very first event then do what ? think
		// Assumption here is we do have time to travel to the first Must to do
		// event. Only we are computing to fit extra events
		if (buffer < 30 * 60 * 1000) {
			Date timeOfEvent;
			if (Second.getType().equals("Must")) {
				timeOfEvent = Second.getStartTime();
			} else {
				timeOfEvent = Utilities.convertMillistoDate(First.getEndTime()
						.getTime() + timeTo2From1);
				Second.setStartTime(timeOfEvent);
				Second.setEndTime(Utilities.convertMillistoDate(First
						.getEndTime().getTime()
						+ timeTo2From1
						+ (30 * 60 * 1000)));
			}
			// Should I have to add it to database? if so now or later ? May be
			// once the user starts the schedule
			// just bath update all the events in the schedule list based on the
			// id
			Second.setState("scheduled");
			Second.setTimeToReachThis(timeTo2From1);
			ScheduleListItem item = new ScheduleListItem(Second, timeOfEvent);
			scheduleList.add(item);
			publishProgress(new String[] { "Adding next item to your schedule" });
			return;
		} else {
			boolean selected = false;
			int count = 0;
			while (selected != true && count < niceToDos.size()) {
				MyEvent selectedEvent = niceToDos.get(count);
				if (selectedEvent.getState().equals("unscheduled")) {
					if (selectedEvent.getLocation() != null) {
						temp = Utilities.getDistance(First.getLocation(),
								selectedEvent.getLocation());
						long timeFrom1ToNew = Long.parseLong(temp[1]);
						temp = Utilities.getDistance(
								selectedEvent.getLocation(),
								Second.getLocation());
						long timeTo2FromNew = Long.parseLong(temp[1]);
						if ((timeFrom1ToNew + (30 * 60 * 1000) + timeTo2FromNew) > timeAvailable) {
							count++;
							continue;
						} else {
							selected = true;
							selectedEvent.setStartTime(Utilities
									.convertMillistoDate(First.getEndTime()
											.getTime() + timeFrom1ToNew));
							fitEnroute(First, selectedEvent);
							fitEnroute(selectedEvent, Second);
							count++;
						}
					} else {
						// determine businesses near Second event in
						// MustToDoList.
						// for now I am assuming a radius of 2 miles = 2 * 1
						// 609.344 metres = approximately 3300 metres
						// check the distance thing again and repeat the above
						// for loop.
						Vector<String> types = new Vector<String>();
						types.add(selectedEvent.getCategory());
						/*
						 * Vector<MyBusiness> listOfBusinessesNearSecond =
						 * Utilities .peekNearByBusinesses(Second.getLocation(),
						 * 3300.0f, types);
						 */
						Vector<MyBusiness> listOfBusinessesNearSecond = Utilities
								.peekNearByBusinessesYelp(Second.getName(),
										Second.getLocation(), 3300.0f,
										types.elementAt(0), types.elementAt(0));
						int length = listOfBusinessesNearSecond.size();
						Log.d("Yelp", Second.getName() + " got back " + length);

						Iterator<MyBusiness> businessItr = listOfBusinessesNearSecond
								.iterator();

						MyBusiness possibleBusiness;
						MyBusiness chosenBusiness = null;
						long min = 0;
						length = listOfBusinessesNearSecond.size();
						boolean first = true;

						while (businessItr.hasNext()) {
							possibleBusiness = businessItr.next();
							temp = Utilities.getDistance(First.getLocation(),
									possibleBusiness.getBusinessLocation());
							long timeFrom1ToThis = Long.parseLong(temp[1]);
							long tempValue = timeFrom1ToThis
									+ (30 * 60 * 1000)
									+ possibleBusiness
											.getTimeToreachIntendedDestination();
							Log.d("Yelp_pre", "businessName="
									+ possibleBusiness.getBusiness_id()
									+ "tempValue=" + tempValue
									+ "tempAvailable=" + timeAvailable);
							if (tempValue < timeAvailable) {

								if (first == true) {
									min = tempValue;
									chosenBusiness = possibleBusiness;
									first = false;
								} else if (tempValue < min
										&& possibleBusiness.getRatings() >= 3.0f) {
									publishProgress(new String[] { "Checking if the business is open" });
									TimeInstant st = new TimeInstant(
											startCal.get(Calendar.HOUR_OF_DAY),
											startCal.get(Calendar.MINUTE));
									endCal = Calendar.getInstance();
									endCal.setTimeInMillis(First.getEndTime()
											.getTime()
											+ timeFrom1ToThis
											+ 30
											* 60 * 1000);
									TimeInstant end = new TimeInstant(
											endCal.get(Calendar.HOUR_OF_DAY),
											endCal.get(Calendar.MINUTE));
									boolean isOpen = new ScrapeYelp(
											possibleBusiness.getBusiness_id())
											.isBusinessOpen(st, end);
									if (!isOpen) {
										businessItr.remove();
										continue;
									}
									Log.d("Yelp_in",
											possibleBusiness.getBusiness_id()
													+ " is open");
									min = tempValue;
									chosenBusiness = possibleBusiness;
								}
								Log.d("Yelp_post", "chosenBusiness="
										+ chosenBusiness.getName());
								selectedEvent
										.setTimeToReachThis(timeFrom1ToThis);
							} else {
								businessItr.remove();
							}

						}

						if (chosenBusiness != null) {
							selected = true;
							selectedEvent.setLocation(chosenBusiness
									.getBusinessLocation());
							selectedEvent
									.setStartTime(Utilities.convertMillistoDate(First
											.getEndTime().getTime()
											+ min
											- (30 * 60 * 1000)
											- chosenBusiness
													.getTimeToreachIntendedDestination()));
							selectedEvent.setEndTime(Utilities
									.convertMillistoDate(selectedEvent
											.getStartTime().getTime()
											+ (30 * 60 * 1000)));
							selectedEvent.setBussName(chosenBusiness.getName());
							selectedEvent
									.setRating(chosenBusiness.getRatings());
							selectedEvent.setImgUrl(chosenBusiness
									.getBusinessImageURL());
							fitEnroute(First, selectedEvent);
							fitEnroute(selectedEvent, Second);
							count++;
						} else {
							count++;
							continue;
						}
					}

				} else {
					count++;
					continue;
				}
			}
			if (selected == false || count >= niceToDos.size()) {
				if (isAddedToSchedule(Second) == false) {
					Date timeOfEvent;
					if (Second.getType().equals("Must")) {
						timeOfEvent = Second.getStartTime();
					} else {
						timeOfEvent = Utilities.convertMillistoDate(First
								.getEndTime().getTime() + timeTo2From1);
						Second.setStartTime(timeOfEvent);
						Second.setEndTime(Utilities.convertMillistoDate(First
								.getEndTime().getTime()
								+ timeTo2From1
								+ (30 * 60 * 1000)));
					}
					// Should I have to add it to database? if so now or later ?
					// May
					// be once the user starts the schedule,
					// just batch update all the events in the schedule list
					// based on
					// the id
					Second.setState("scheduled");
					Second.setTimeToReachThis(timeTo2From1);
					scheduleList.add(new ScheduleListItem(Second, timeOfEvent));
					publishProgress(new String[] { "Adding next item to your schedule" });
				}
				return;
			}
		}

	}

	/**
	 * checks if already added to schedule
	 * 
	 * @param second
	 * @return
	 */
	private boolean isAddedToSchedule(MyEvent second) {
		// in case of repetition of events am not sure how to change id of a
		// repeating event daily becase I am searching the events basd on the
		// assuption that id is different

		int count = scheduleList.size();
		for (int i = 0; i < count; i++) {
			MyEvent e = scheduleList.get(i).getEvent();

			if (e.getId() == second.getId()
					&& !second.getName().equals("EndEvent")) {
				return true;
			}
			if (e.getName().equals("EndEvent")
					&& second.getName().equals("EndEvent")) {
				return true;
			}
		}
		return false;
	}

	/**
	 * we want to let the user know that he can reach even earlier than what he
	 * plans (if that is possible). Basically rechecks the last driving time
	 * between last two events in the schedule
	 */
	public void runScheduleCompaction() {
		int size = scheduleList.size();
		MyEvent lastEvent = scheduleList.get(size - 1).getEvent();
		MyEvent secondToLastEvent = scheduleList.get(size - 2).getEvent();
		Date st = Utilities.convertMillistoDate(secondToLastEvent.getEndTime()
				.getTime() + lastEvent.getTimeToReachThis());
		lastEvent.setStartTime(st);
		lastEvent.setEndTime(lastEvent.getEndTime());
		scheduleList.get(size - 1).setStartTime(st);
	}

	@Override
	protected void onPreExecute() {
		super.onPreExecute();
		progressDialog = new ProgressDialog(context);
		progressDialog.setMessage("Computing the schedule");
		progressDialog.show();
	}

	@Override
	protected ArrayList<ScheduleListItem> doInBackground(Object... params) {

		ArrayList<ScheduleListItem> schedule = new ArrayList<ScheduleListItem>();
		try {
			schedule = computeSchedule();
			Utilities.setSchedule(schedule);
			// publishProgress(schedule);
		} catch (JSONException e) {
			e.printStackTrace();
		}
		return schedule;
	}

	@Override
	protected void onProgressUpdate(String... msg) {
		progressDialog.setMessage(msg[0]);
	}

	@Override
	protected void onPostExecute(ArrayList<ScheduleListItem> schedule) {
		progressDialog.dismiss();
		callerActivity.updateScheduleDisplay(schedule);
		super.onPostExecute(schedule);
	}

}
